import { app } from '../../scripts/app'

// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys

app.registerExtension({
  name: 'Comfy.EditAttention',
  init() {
    const editAttentionDelta = app.ui.settings.addSetting({
      id: 'Comfy.EditAttention.Delta',
      category: ['Comfy', 'EditTokenWeight', 'Delta'],
      name: 'Ctrl+up/down precision',
      type: 'slider',
      attrs: {
        min: 0.01,
        max: 0.5,
        step: 0.01
      },
      defaultValue: 0.05
    })

    function incrementWeight(weight: string, delta: number): string {
      const floatWeight = parseFloat(weight)
      if (isNaN(floatWeight)) return weight
      const newWeight = floatWeight + delta
      return String(Number(newWeight.toFixed(10)))
    }

    type Enclosure = {
      start: number
      end: number
    }

    function findNearestEnclosure(
      text: string,
      cursorPos: number
    ): Enclosure | null {
      let start = cursorPos,
        end = cursorPos
      let openCount = 0,
        closeCount = 0

      // Find opening parenthesis before cursor
      while (start >= 0) {
        start--
        if (text[start] === '(' && openCount === closeCount) break
        if (text[start] === '(') openCount++
        if (text[start] === ')') closeCount++
      }
      if (start < 0) return null

      openCount = 0
      closeCount = 0

      // Find closing parenthesis after cursor
      while (end < text.length) {
        if (text[end] === ')' && openCount === closeCount) break
        if (text[end] === '(') openCount++
        if (text[end] === ')') closeCount++
        end++
      }
      if (end === text.length) return null

      return { start: start + 1, end: end }
    }

    function addWeightToParentheses(text: string): string {
      const parenRegex = /^\((.*)\)$/
      const parenMatch = text.match(parenRegex)

      const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/
      const floatMatch = text.match(floatRegex)

      if (parenMatch && !floatMatch) {
        return `(${parenMatch[1]}:1.0)`
      } else {
        return text
      }
    }

    function editAttention(event: KeyboardEvent) {
      // @ts-expect-error Runtime narrowing not impl.
      const inputField: HTMLTextAreaElement = event.composedPath()[0]
      const delta = parseFloat(editAttentionDelta.value)

      if (inputField.tagName !== 'TEXTAREA') return
      if (!(event.key === 'ArrowUp' || event.key === 'ArrowDown')) return
      if (!event.ctrlKey && !event.metaKey) return

      event.preventDefault()

      let start = inputField.selectionStart
      let end = inputField.selectionEnd
      let selectedText = inputField.value.substring(start, end)

      // If there is no selection, attempt to find the nearest enclosure, or select the current word
      if (!selectedText) {
        const nearestEnclosure = findNearestEnclosure(inputField.value, start)
        if (nearestEnclosure) {
          start = nearestEnclosure.start
          end = nearestEnclosure.end
          selectedText = inputField.value.substring(start, end)
        } else {
          // Select the current word, find the start and end of the word
          const delimiters = ' .,\\/!?%^*;:{}=-_`~()\r\n\t'

          while (
            !delimiters.includes(inputField.value[start - 1]) &&
            start > 0
          ) {
            start--
          }

          while (
            !delimiters.includes(inputField.value[end]) &&
            end < inputField.value.length
          ) {
            end++
          }

          selectedText = inputField.value.substring(start, end)
          if (!selectedText) return
        }
      }

      // If the selection ends with a space, remove it
      if (selectedText[selectedText.length - 1] === ' ') {
        selectedText = selectedText.substring(0, selectedText.length - 1)
        end -= 1
      }

      // If there are parentheses left and right of the selection, select them
      if (
        inputField.value[start - 1] === '(' &&
        inputField.value[end] === ')'
      ) {
        start -= 1
        end += 1
        selectedText = inputField.value.substring(start, end)
      }

      // If the selection is not enclosed in parentheses, add them
      if (
        selectedText[0] !== '(' ||
        selectedText[selectedText.length - 1] !== ')'
      ) {
        selectedText = `(${selectedText})`
      }

      // If the selection does not have a weight, add a weight of 1.0
      selectedText = addWeightToParentheses(selectedText)

      // Increment the weight
      const weightDelta = event.key === 'ArrowUp' ? delta : -delta
      const updatedText = selectedText.replace(
        /\((.*):([+-]?\d+(?:\.\d+)?)\)/,
        (_, text, weight) => {
          weight = incrementWeight(weight, weightDelta)
          if (weight == 1) {
            return text
          } else {
            return `(${text}:${weight})`
          }
        }
      )

      inputField.setSelectionRange(start, end)
      // Intentional use of deprecated: https://developer.mozilla.org/docs/Web/API/Document/execCommand#using_inserttext
      document.execCommand('insertText', false, updatedText)
      inputField.setSelectionRange(start, start + updatedText.length)
    }
    window.addEventListener('keydown', editAttention)
  }
})
